1.概述 | 2.我的第一个项目 | 3.模型 | 4.视图 | 5.列表数据 | 6.对象/关系映射 | 7.控制器 | 8.应用 | 9.定制

第3章: 模型

在面向对象应用程序的模型层中包含业务逻辑,也就是数据结构和与数据有关的计算、校验和处理过程。
OpenXava 是一个面向模型的框架,其中模型最为重要,而其他的(例如,用户界面)依赖于模型。
OpenXava 定义模型的方式是使用纯Java类 (也有一个 XML 版本)。 根据你的模型定义,OpenXava 生成全功能的应用系统。

业务组件(Business Component)

创建OpenXava应用系统的基本单元是业务组件(business component)。 业务组件由名为Entity的Java 类定义 . 这个类是标准的 EJB3 实体, 或者说, 是一个 包含有遵循 Java Persistence API (JPA) 标准注解的POJO 类.
JPA 是Java持久性标准, 用于对象将其状态保存到数据库中。如果你知道如何用 POJOs 和 JPA开发, 那你就已经知道如何开发OpenXava应用程序了。
使用一个简单的Java类,你就可以定义一个业务组件,使用:
  • 模型: 数据结构, 校验, 计算等.
  • 视图: 显示给用户看的模型是什么样.
  • 列表数据: 组件中的数据在列表模式下什么样.
  • 对象/关系映射: 如何在数据库中存取对象的状态.
在本章中对模型部分进行说明, 包括:结构、校验、计算等。

实体(Entity)

要定义模型部分,你必须定义一个带注解的java类。除了自己的注解之外,OpenXava还支持JPAHibernate Validator 注解。这样的java类就是一个实体,也就是一个代表业务概念的持久化类。
在本章中JPA代表standard Java Persistent API annotations,HV代表Hibernate Validator annotation,OX代表an annotation ofOpenXava。
实体语法如下:
@Entity                     //  1
@EntityValidator            //  2
@RemoveValidator            //  3
public class EntityName {   //  4
  // Properties             //  5
  // References             //  6
  // Collections            //  7
  // Methods                //  8
  // Finders                //  9
  // Callback methods       // 10
}
  1. @Entity (JPA, one, required): 标识这个类是一个 JPA entity, 换句话说, 它的实例是持久化对象.
  2. @EntityValidator (OX, several, optional):在模型层执行的校验器(validator).校验器可以获取多个模型属性值. 要校验一个属性,最好用属性层校验器。
  3. @RemoveValidator (OX, several, optional): 在移除前执行,可以驳回对象的移除操作.
  4. Class declaration: 标准的java类. 你可以使用extendsimplements.
  5. Properties: 标准的java属性. 代表对象的主要状态.
  6. References: 引用其他对象.
  7. Collections: 对其他对象引用的集合.
  8. Methods: 包含业务逻辑的java方法.
  9. Finders: Finder 方法是使用 JPA query facilities进行查询的静态方法.
  10. Callback methods: JPA 回调函数,用于在创建、修改、加载、移除中插入业务逻辑。

属性

属性(Properties)代表对象的状态,该状态可以读取,有时也可以更新。对象不负责属性数据的物理存储,只在需要时返回该值。
定义属性的语法如下:
@Stereotype                                                  //  1
@Column(length=) @Max @Length(max=) @Digits(integerDigits=)  //  2
@Digits(fractionalDigits=)                                   //  3
@Required @Min @Range(min=) @Length(min=)                    //  4
@Id                                                          //  5
@Hidden                                                      //  6
@SearchKey                                                   //  7
@Version                                                     //  8
@DefaultValueCalculator                                      //  9
@PropertyValidator                                           // 10
private type propertyName;                                   // 11
public type getPropertyName() { ... }                        // 11
public void setPropertyName(type newValue) { ... }           // 11
  1. @Stereotype (OX, 可选): 版型,可以定义某些对象的特定行为。
  2. @Column(length=) (JPA), @Max (HV), @Length(max=) (HV), @Digits(integerDigits=) (HV, 可选, 通常你只需要使用其中一个): 属性中的字符长度, @Max 指的是最大值. 用于生成用户界面. 如果你不指定大小, 那么就会使用缺省值. 缺省值与类型或版型相关,存储在 default-size.xml文件中.
  3. @Digits(fractionalDigits=) (HV, 可选): 属性的精度(小数部分的位数) . 只适用于数值型属性. 如果你不指定精度,就使用缺省精度. 缺省值与类型或者版型相关,存储在default-size.xml文件中.
  4. @Required (OX), @Min (HV), @Range(min=) (HV), @Length(min=) (HV) (可选, 通常你只需要使用其中一个): 表明这个属性是必需的. 在@Min, @Range@Length 中你需要为min指定一个大于零的数值来标明属性是必需的。缺省状态下主键属性是必需的,(new in v2.1.3) 其他属性则不是必需的. 在保存是, OpenXava 检验所有必需属性的存在. 如果不满足, 保存过程中止,返回一个校验错误列表。判定属性存在与否的逻辑可以通过在项目中创建一个名为 validators.xml 的文件来实现. 语法参照 OpenXava/xava/validators.xml. @Required 是 Hibernate Validator 约束 (new in v3.0.1).
  5. @Id (JPA, 可选): 标志本属性是主键的一部分. 至少有一个属性(或引用)必须为主键. 键属性组合 (或键引用组合) 必须映射为数据表中不存在重复值的列组合,通常是数据表的主键.
  6. @Hidden (OX, 可选): 隐藏属性对程序员有意义,但对用户无意义. 在用户界面自动生成时,隐藏属性被排除在外.但在Java 代码层面,隐藏属性存在并可正常使用. 而且,如果你在视图中进行显式指定,属性也能出现在用户界面中。.
  7. @SearchKey (OX, 可选): 用户可以将查找键属性用作查找对象的键。 在用户界面中可以编辑,允许用户键入值进行查找。 OpenXava 缺省使用 @Id 属性进行查找, 如果id 属性隐藏,就使用视图中的第一个属性。 通过定义@SearchKey ,你可以明确选定用于查找的属性。
  8. @Version (JPA, 可选): 版本属性用于开放式并发控制. 如果你想控制并发,只需要在你的实体中设置一个属性标记为 @Version . 一个实体只能有一个版本属性. 版本属性支持下列类型: int, Integer, short, Short, long, Long, Timestamp. 版本属性是隐藏的.
  9. @DefaultValueCalculator (OX, 一个, 可选):实现计算属性缺省(初始)值的逻辑. 带 @DefaultValueCalculator 的属性有 setter 而且是可持久化的.
  10. @PropertyValidator (OX, 多个, 可选): 实现属性校验逻辑,在修改或创建包含该对象的对象时执行。
  11. Property declaration: 带有getter 和 setter的正规Java属性声明. 你可以创建一个只有getter而没有对应字段和setter的计算属性. JPA的所有合法类型都可用, 你只需要提供 Hibernate 类型在数据库中进行保存,OpenXava编辑器来将其渲染为 HTML即可.

版型(Stereotype)

版型 (@Stereotype) 是指定类型特定行为的一种方式. 例如, 名称(name), 备注(comment), 描述(description)等 都对应到Java 类型 java.lang.String ,但你期望每种情况下校验器、缺省大小,可视化编辑器都有所不同;如果你想进行更精确的调整,你可以指定版型。这就是说, 你可以使用这样一些版型 NAME, MEMO or DESCRIPTION对属性进行设置。
OpenXava 包含下列通用版型:
  • DINERO, MONEY
  • FOTO, PHOTO, IMAGEN, IMAGE
  • TEXTO_GRANDE, MEMO, TEXT_AREA
  • ETIQUETA, LABEL
  • ETIQUETA_NEGRITA, BOLD_LABEL
  • HORA, TIME
  • FECHAHORA, DATETIME
  • GALERIA_IMAGENES, IMAGES_GALLERY (setup instructions)
  • RELLENADO_CON_CEROS, ZEROS_FILLED
  • TEXTO_HTML, HTML_TEXT (text with editable format)
  • IMAGE_LABEL, ETIQUETA_IMAGEN (image depending on property content)
  • EMAIL
  • TELEFONO, TELEPHONE
  • WEBURL
  • IP
  • ISBN
  • TARJETA_CREDITO, CREDIT_CARD
  • LISTA_EMAIL, EMAIL_LIST
下面展示如何定义你自己的版型. 创建一个 名为PERSON_NAME的版型来标识人员姓名.
编辑(或创建) 在目录xava下的文件 editors.xml . 添加:
<editor url="personNameEditor.jsp">
    <for-stereotype stereotype="PERSON_NAME"/>
</editor>
这样你就定义来编辑器来对版型为 PERSON_NAME的属性来进行编辑或显示.
下一步,定义缺省大小非常有用; 你可以编辑项目中的 default-size.xml文件:
<for-stereotype name="PERSON_NAME" size="40"/>
这样,如果你不指定 PERSON_NAME 类型属性的大小,就会使用40作为缺省值.
一般情况下不需要改变校验器,但如果你确实需要,你可以在在项目的 validators.xml 文件中添加:
<required-validator>
    <validator-class class="org.openxava.validators.NotBlankCharacterValidator"/>
    <for-stereotype stereotype="PERSON_NAME"/>
</required-validator>
现在一切就绪,你可以将属性定义为PERSON_NAME版型了:
@Stereotype("PERSON_NAME")
private String name;
在这个例子中, 属性的大小为40 , 类型为String, 如需要NotBlankCharacterValidator 校验器将执行.

IMAGES_GALLERY 版型

如果你希望组件中的一个属性能够包含图片.你只需要将属性声明为 IMAGES_GALLERY 版型, 像这样:
@Stereotype("IMAGES_GALLERY")
private String photos;
此外, 在映射部分你必须将属性映射到适合存储长度为32为的字符的表列上。
然后,一切完成.
要支持这种版型,你必须对系统进行配置。
首先, 在数据库中创建一个表用于图片存储:
CREATE TABLE IMAGES (
    ID VARCHAR(32) NOT NULL PRIMARY KEY,
    GALLERY VARCHAR(32) NOT NULL,
    IMAGE BLOB);
CREATE INDEX IMAGES01
    ON IMAGES (GALLERY);
 
IMAGE 列的类型可以选择你的数据库中存储byte []更合适的类型 (例如 LONGVARBINARY) .
最后,你需要在 persistence/hibernate.cfg.xml 文件中定义映射:
<hibernate-configuration>
    <session-factory>
        ...
        <mapping resource="GalleryImage.hbm.xml"/>
        ...
    </session-factory>
</hibernate-configuration>
这样你就可以在应用的所有组件中使用IMAGES_GALLERY版型了.

并发和版本属性

并发是应用的一种能力,允许多个用户同时保存数据而不造成数据丢失. OpenXava 使用JPA的开放式并发控制机制. 使用开放式并发控制,记录并不进行锁定,允许高并发而不影响数据的完整性。
例如, 如果用户 A 读取了一条记录,然后用户B读取同一条记录,用户B修改记录并保存改变,当用户 A视图保存记录时会收到一条错误信息,他必须重新刷新数据后来进行修改.
要激活实体的并发支持,你只需要定义属性 @Version, 以如下方式:
@Version
private int version;
本属性由持久化引擎使用, 你的应用或用户不能直接使用这一属性.

枚举

OpenXava支持 Java 5 枚举类型. 枚举允许你定义只包含指示值之一的属性.
枚举非常易于使用, 例如:
private Distance distance;
public enum Distance { LOCAL, NATIONAL, INTERNATIONAL };
 
distance 属性只能取这样几个值: LOCAL, NATIONAL 或INTERNATIONAL, 如果不指定 @Required 没有值 (null) 也是允许的.
在用户界面层面,当前的实现使用了下拉框.每个值的标签从i18n 文件中获取.
在数据库层面,值缺省保存为 整型 (0 代表LOCAL, 1 代表NATIONAL, 2 代表INTERNATIONAL ,而null 代表没有取值), 但你也可以配置为其他类型, 在遗留数据库中可以正常使用. 参见 映射 章节.

计算属性

计算属性是只读的 (只有 getter),不能持久化(不对应数据表中的任何列).
计算属性用下列方式定义:
@Depends("unitPrice")  // 1
@Max(9999999999L)      // 2
public BigDecimal getUnitPriceInPesetas() {
    if (unitPrice == null) return null;
    return unitPrice.multiply(new BigDecimal("166.386")).setScale(0, BigDecimal.ROUND_HALF_UP);
}
 
根据上述定义,你可以在代码中这样使用:
Product product = ...
product.setUnitPrice(2);
BigDecimal result = product.getUnitPriceInPesetas();
 
result 就是 332.772.
属性unitPriceInPesetas展示给用户时是不可编辑的, 编辑器的长度为 10,由@Max(9999999999L) (2)指定. 同时,因为你使用了 @Depends("unitPrice") (1) ,当用户在用户界面中改变unitPrice 属性的值时, unitPriceInPesetas属性将重新计算,值将刷新显示给用户.
在计算属性中,你可以直接访问 JDBC 连接, 例如:
@Max(999)
public int getDetailsCount() {
    // 使用 JDBC的例子
    Connection con = null;
    try {
        con = DataSourceConnectionProvider.getByComponent("Invoice").getConnection();  // 1
        String table = MetaModel.get("InvoiceDetail").getMapping().getTable();
        PreparedStatement ps = con.prepareStatement("select count(*) from " + table +
            " where INVOICE_YEAR = ? and INVOICE_NUMBER = ?");
        ps.setInt(1, getYear());
        ps.setInt(2, getNumber());
        ResultSet rs = ps.executeQuery();
        rs.next();
        Integer result = new Integer(rs.getInt(1));
        ps.close();
        return result;
    }
    catch (Exception ex) {
        log.error("Problem calculating details count of an Invoice", ex);
        // You can throw any runtime exception here
        throw new SystemException(ex);
    }
    finally {
        try {
            con.close();
        }
        catch (Exception ex) {
        }
    }
}
 
JDBC 代码确实丑陋笨拙,但有时确实有助于解决性能问题. DataSourceConnectionProvider 类允许你获取实体数据源的连接 (Invoice in this case).这个类只是为你提供方便, 你也可以使用JNDI或其他方式获取 JDBC连接.事实上,在计算属性中,你可以编写任何Java代码.
如果你使用基于属性的访问, 也就是注解类的getter或 setters, 你必须添加 @Transient 注解到对应的计算属性, 用这种方式:
private long number;
 
@Id @Column(length=10)     //注解getter,
public long getNumber() {  // JPA will use property-base access for your class
    return number;
}
public void setNumber(long number) {
    this.number = number;
}
 
@Transient                    // You have to annotate as Transient your calculated property
public String getZoneOne() {  // because you are using property-based access
    return "In ZONE 1";
}
 

缺省值计算器

使用@DefaultValueCalculator 你可以在属性上关联业务逻辑, 这种情况下属性是可读可写的.缺省值计算器用于计算初始值. 例如:
@DefaultValueCalculator(CurrentYearCalculator.class)
private int year;
 
当用户要创建一个新的Invoice (举例来说),他会发现year 字段已经有值了,他也可以进行修改. 生成值的而逻辑在 CurrentYearCalculator 类中:
package org.openxava.calculators;
 
import java.util.*;
 
/**
 * @author Javier Paniza
 */
public class CurrentYearCalculator implements ICalculator {
 
    public Object calculate() throws Exception {
        Calendar cal = Calendar.getInstance();
        cal.setTime(new java.util.Date());
        return new Integer(cal.get(Calendar.YEAR));
    }
 
}
 
通过设置属性值,可以定制计算器的行为:
@DefaultValueCalculator(
    value=org.openxava.calculators.StringCalculator.class,
    properties={ @PropertyValue(name="string", value="GOOD") }
)
private String relationWithSeller;
在这种情况下, 计算缺省值时OpenXava首先 实例化StringCalculator然后将值 "GOOD" 注入 StringCalculatorstring属性 , 最后调用 calculate() 方法来获取 relationWithSeller的缺省值. 你可以看出, 使用 @PropertyValue 注解,你可以创建可重用的计算器。
@PropertyValue 允许你从其他已显示的属性中注入值, 如是:
@DefaultValueCalculator(
    value=org.openxava.test.calculators.CarrierRemarksCalculator.class,
    properties={
        @PropertyValue(name="drivingLicenceType", from="drivingLicence.type")
    }
)
private String remarks;
在这种情况下,执行计算器之前,OpenXava会将引用的drivingLicence的type属性的值注入CarrierRemarksCalculatordrivingLicenceType属性,As you see the from attribute supports qualified properties (reference.property).
@PropertyValue同样允许属性的值为空:
@DefaultValueCalculator(value=DefaultProductPriceCalculator.class, properties=
    @PropertyValue(name="familyNumber")
)
In this case OpenXava takes the value of the displayed property familyNumber and inject it in the property familyNumber of the calculator; that is @PropertyValue(name="familiyNumber") is equivalent to @PropertyValue(name="familiyNumber", from="familyNumber").
From a calculator you have direct access to JDBC connections, here is an example:
@DefaultValueCalculator(value=DetailsCountCalculator.class,
    properties= {
        @PropertyValue(name="year"),
        @PropertyValue(name="number"),
    }
)
private int detailsCount;
 
And the calculator class:
package org.openxava.test.calculators;
 
import java.sql.*;
 
import org.openxava.calculators.*;
import org.openxava.util.*;
 
/**
 * @author Javier Paniza
 */
public class DetailsCountCalculator implements IJDBCCalculator {      // 1
 
    private IConnectionProvider provider;
    private int year;
    private int number;
 
    public void setConnectionProvider(IConnectionProvider provider) { // 2
        this.provider = provider;
    }
 
    public Object calculate() throws Exception {
        Connection con = provider.getConnection();
        try {
            PreparedStatement ps = con.prepareStatement(
                "select count(*) from XAVATEST.INVOICEDETAIL “ +
                “where INVOICE_YEAR = ? and INVOICE_NUMBER = ?");
            ps.setInt(1, getYear());
            ps.setInt(2, getNumber());
            ResultSet rs = ps.executeQuery();
            rs.next();
            Integer result = new Integer(rs.getInt(1));
            ps.close();
            return result;
        }
        finally {
            con.close();
        }
    }
 
    public int getYear() {
        return year;
    }
 
    public int getNumber() {
        return number;
    }
 
    public void setYear(int year) {
        this.year = year;
    }
 
    public void setNumber(int number) {
        this.number = number;
    }
 
}
To use JDBC your calculator must implement IJDBCCalculator (1) and then it will receive an IConnectionProvider (2) that you can use within calculate().
OpenXava comes with a set of predefined calculators, you can find them in org.openxava.calculators.

创建时的默认值

You can indicate that the value will be calculated just before creating (inserting into database) an object for the first time.
Usually for the key case you use the JPA standard. For example, if you want to use an identity (auto increment) column as key:
@Id @Hidden
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
 
You can use other generation techniques, for example, a database sequence can be defined in this JPA standard way:
@SequenceGenerator(name="SIZE_SEQ", sequenceName="SIZE_ID_SEQ", allocationSize=1 )
@Hidden @Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="SIZE_SEQ")
private Integer id;
 
If you want to generate a unique identifier of type String and 32 characters, you can use a Hibernate extesion of JPA:
@Id @GeneratedValue(generator="system-uuid") @Hidden
@GenericGenerator(name="system-uuid", strategy = "uuid")
private String oid;
 
Look at section 9.1.9 of JPA 1.0 specification (part of JSR-220) for learning more about @GeneratedValues.
If you want to use your own logic for generating the value on create, or you want a generated new value for a non-key property then you cannot use the JPA @GeneratedValue, although it's easy to solve these cases using JPA. You only need to add this code to your class:
@PrePersist
private void calculateCounter() {
    counter = new Long(System.currentTimeMillis()).intValue();
}
 
The JPA @PrePersist annotation does that this method will be executed before inserting the data the first time in database, in this method you can calculate the value for your key or non-key properties with your own logic.

属性校验器

A @PropertyValidator executes validation logic on the value assigned to the property just before storing. A property may have several validators:
@PropertyValidators ({
    @PropertyValidator(value=ExcludeStringValidator.class, properties=
        @PropertyValue(name="string", value="MOTO")
    ),
    @PropertyValidator(value=ExcludeStringValidator.class, properties=
        @PropertyValue(name="string", value="COCHE"),
        onlyOnCreate=true
    )
})
private String description;
 
The technique to configure the validator (with @PropertyValue) is exactly the same than in calculators. With the attribute onlyOnCreate=”true” you can define that the validation will be executed only when the object is created, and not when it is modified.
The validator code is:
package org.openxava.test.validators;
 
import org.openxava.util.*;
import org.openxava.validators.*;
 
/**
 * @author Javier Paniza
 */
 
public class ExcludeStringValidator implements IPropertyValidator { // 1
 
    private String string;
 
    public void validate(
        Messages errors,                                           // 2
        Object value,                                              // 3
        String objectName,                                         // 4
        String propertyName)                                       // 5
        throws Exception {
        if (value==null) return;
        if (value.toString().indexOf(getString()) >= 0) {
            errors.add("exclude_string", propertyName, objectName, getString());
        }
    }
 
    public String getString() {
        return string==null?"":string;
    }
 
    public void setString(String string) {
        this.string = string;
    }
 
}
A validator has to implement IPropertyValidator (1), this obliges to the calculator to have a validate() method where the validation of property is executed. The arguments of validate() method are:
  1. Messages errors: A object of type Messages that represents a set of messages (like a smart collection) and where you can add the validation errors that you find.
  2. Object value: The value to validate.
  3. String objectName: Object name of the container of the property to validate. Useful to use in error messages.
  4. String propertyName: Name of the property to validate. Useful to use in error messages.
As you can see when you find a validation error you have to add it (with errors.add()) by sending a message identifier and the arguments. If you want to obtain a significant message you need to add to your i18n file the next entry:
exclude_string={0} cannot contain {2} in {1}
If the identifier sent is not found in the resource file, this identifier is shown as is; but the recommended way is always to use identifiers of resource files.
The validation is successful if no messages are added and fails if messages are added. OpenXava collects all messages of all validators before saving and if there are messages, then it display them and does not save the object.
The package org.openxava.validators contains some common validators.
@PropertyValidator is defined as a Hibernate Validator contraint (new in v3.0.1).

默认校验器(new in v2.0.3)

You can define a default validator for properties depending of its type or stereotype. In order to do it you have to use the file xava/validators.xml of your project to define in it the default validators.
For example, you can define in your xava/validators.xml the following:
<validators>
    <default-validator>
        <validator-class
            class="org.openxava.test.validators.PersonNameValidator"/>
        <for-stereotype stereotype="PERSON_NAME"/>
    </default-validator>
</validators>
In this case you are associating the validator PersonNameValidator to the stereotype PERSON_NAME. Now if you define a property as the next one:
@Required @Stereotype("PERSON_NAME")
private String name;
This property will be validated using PersonNameValidator although the property itself does not define any validator. PersonNameValidator is applied to all properties with PERSON_NAME stereotype.
You can also assign a default validator to a type.
In validators.xml files you can also define the validators for determine if a required value is present (executed when you use @Required). Moreover you can assign names (alias) to validator classes.
You can learn more about validators examining OpenXava/xava/validators.xml and OpenXavaTest/xava/validators.xml.
Default validators do not apply when you use directly the JPA api for saving your entities.

参照(reference)

A reference allows access from an entity to another entity. A reference is translated to Java code as a property (with its getter and its setter) whose type is the referenced model Java type. For example a Customer can have a reference to his Seller, and that allows you to write code like this:
Customer customer = ...
customer.getSeller().getName();
to access to the name of the seller of that customer.
The syntax of reference is:
@Required                                            // 1
@Id                                                  // 2
@SearchKey                                           // 3  New in v3.0.2
@DefaultValueCalculator                              // 4
@ManyToOne(                                          // 5
    optional=false                                   // 1
)
private type referenceName;                          // 5
public type getReferenceName() { ... }               // 5
public void setReferenceName(type newValue) { ... }  // 5
  1. @ManyToOne(optional=false) (JPA), @Required (OX) (optional, the JPA is the preferred one): Indicates if the reference is required. When saving OpenXava verifies if the required references are present, if not the saving is aborted and a list of validation errors is returned.
  2. @Id (JPA, optional): Indicates if the reference is part of the key. The combination of key properties and reference properties should map to a group of database columns with unique values, typically the primary key.
  3. @SearchKey (OX, optional): (New in v3.0.2) The search key references are used by the user as key for searching objects. They are editable in user interface of references allowing to the user type its value for searching. OpenXava uses the @Id members for searching by default, and if the id members are hidden then it uses the first property in the view. With @SearchKey you can choose explicitly references for searching.
  4. @DefaultValueCalculator (OX, one, optional): Implements the logic for calculating the initial value of the reference. This calculator must return the key value, that can be a simple value (only if the key of referenced object is simple) or key object (a special object that wraps the key).
  5. Reference declaration: A regular Java reference declaration with its getters and setters. The reference is marked with @ManyToOne (JPA) and the type must be another entity.
A little example of references:
@ManyToOne
private Seller seller;                  // 1
public Seller getSeller() {
    return seller;
}
public void setSeller(Seller seller) {
    this.seller = seller;
}
 
@ManyToOne(fetch=FetchType.LAZY)
private Seller alternateSeller;         // 2
public Seller getAlternateSeller() {
    return alternateSeller;
}
public void setAlternateSeller(Seller alternateSeller) {
    this.alternateSeller = alternateSeller;
}
 
 
  1. A reference called seller to the entity of Seller entity.
  2. A reference called alternateSeller to the entity Seller. In this case we use fetch=FetchType.LAZY, in this way the data is readed from database on demand. This is the more efficient approach, but it's not the JPA default, therefore it's advisable to use always fetch=FetchType.LAZY when declaring the references.
If you assume that this is in an entity named Customer, you could write:
Customer customer = ...
Seller seller = customer.getSeller();
Seller alternateSeller = customer.getAlternateSeller();
 

参照中的默认值计算器

In a reference @DefaultValueCalculator works like in a property, only that it has to return the value of the reference key.
For example, in the case of a reference with simple key, you can write:
@ManyToOne(optional=false, fetch=FetchType.LAZY) @JoinColumn(name="FAMILY")
@DefaultValueCalculator(value=IntegerCalculator.class, properties=
    @PropertyValue(name="value", value="2")
)
private Family family;
 
The calculate() method is:
public Object calculate() throws Exception {
    return new Integer(value);
}
 
As you can see an integer is returned, that is, the default value for family is 2.
In the case of composed key:
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumns({
    @JoinColumn(name="ZONE", referencedColumnName="ZONE"),
    @JoinColumn(name="WAREHOUSE", referencedColumnName="NUMBER")
})
@DefaultValueCalculator(DefaultWarehouseCalculator.class)
private Warehouse warehouse;
 
And the calculator code:
package org.openxava.test.calculators;
 
import org.openxava.calculators.*;
 
/**
 * @author Javier Paniza
 */
public class DefaultWarehouseCalculator implements ICalculator {
 
    public Object calculate() throws Exception {
        WarehouseKey key = new WarehouseKey();
        key.setNumber(4);
        key.setZoneNumber(4);
        return key;
    }
 
}
 
Returns an object of type WarehouseKey.

将参照作为键

You can use references as key, or as part of the key. You have to declare the reference as @Id, and use an id class, as following:
@Entity
@IdClass(AdditionalDetailKey.class)
public class AdditionalDetail {
 
    // JoinColumn is also specified in AditionalDetailKey because
    // a bug in Hibernate, see http://opensource.atlassian.com/projects/hibernate/browse/ANN-361
    @Id @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name="SERVICE")
    private Service service;
 
    @Id @Hidden
    private int counter;
 
    ...
 
}
Also, you need to write your key class:
public class AdditionalDetailKey implements java.io.Serializable {
 
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name="SERVICE")
    private Service service;
 
    @Hidden
    private int counter;
 
    // equals, hashCode, toString, getters and setters
    ...
 
 }
You need to write the key class although the key would be only a reference with only a join column.
It's better to use this feature only when you are working against legate databases, if you have control over the schema use an autogenerated id instead.

集合(Collections)

You can define a collection of references to entities. A collection is a Java property of type java.util.Collection.
Here syntax for collection:
@Size                                                       // 1
@Condition                                                  // 2
@OrderBy                                                    // 3
@XOrderBy                                                   // 4
@OneToMany/@ManyToMany                                      // 5
private Collection collectionName;                          // 5
public Collection getCollectionName() { ... }               // 5
public void setCollectionName(Collection newValue) { ... }  // 5
 
  1. @Size (HV, optional): Minimum (min) and/or maximum (max) number of expected elements. This is validated just before saving.
  2. @Condition (OX, optional): Restricts the elements that appear in the collection.
  3. @OrderBy (JPA, optional): The elements in collections will be in the indicated order.
  4. @XOrderBy (OX, optional): The @OrderBy of JPA does not allow to use qualified properties (properties of references). @XOrderBy does allow it.
  5. Collection declaration: A regular Java collection declaration with its getters and setters. The collection is marked with @OneToMany (JPA) or @ManyToMany (JPA) and the type must be another entity.
Let's have a look at some examples. First a simple one:
@OneToMany (mappedBy="invoice")
private Collection deliveries;
public Collection getDeliveries() {
    return deliveries;
}
public void setDeliveries(Collection deliveries) {
    this.deliveries = deliveries;
}
If you have this within an Invoice, then you are defining a deliveries collection associated to that Invoice. The details to make the relationship are defined in the object/relational mapping.You use mappedBy="invoice" to indicate that the reference invoice of Delivery is used to mapping this collection.
Now you can write a code like this:
Invoice invoice = ...
for (Delivery delivery: invoice.getDeliveries()) {
    delivery.doSomething();
}
To do something with all deliveries associated to an invoice.
Let's look at another example a little more complex, but still in Invoice:
@OneToMany (mappedBy="invoice", cascade=CascadeType.REMOVE)  // 1
@OrderBy("serviceType desc")                                 // 2
@org.hibernate.validator.Size(min=1)                         // 3
private Collection details;
  1. Using REMOVE as cascade type produces that when the user removes an invoice its details are also removed.
  2. With @OrderBy you force that the details will be returned ordered by serviceType.
  3. The restriction @Size(min=1) requires at least one detail for the invoice to be valid.
You have full freedom to define how the collection data is obtained, with @Condition you can overwrite the default condition:
@Condition(
    "${warehouse.zoneNumber} = ${this.warehouse.zoneNumber} AND " +
    "${warehouse.number} = ${this.warehouse.number} AND " +
    "NOT (${number} = ${this.number})"
)
public Collection getFellowCarriers() {
    return null;
}
 
If you have this collection within Carrier, you can obtain with this collection all carriers of the same warehouse but not himself, that is the list of his fellow workers. As you see you can use this in the condition in order to reference the value of a property of current object. @Condition only applied to the user interface generated by OpenXava, if you call directly to getFellowCarriers() it will be returns null.
If with this you have not enough, you can write the logic that returns the collection. The previous example can be written in the following way too:
public Collection getFellowCarriers() {
    Query query = XPersistence.getManager().createQuery("from Carrier c where " +
        "c.warehouse.zoneNumber = :zone AND " +
        "c.warehouse.number = :warehouseNumber AND " +
        "NOT (c.number = :number) ");
    query.setParameter("zone", getWarehouse().getZoneNumber());
    query.setParameter("warehouseNumber", getWarehouse().getNumber());
    query.setParameter("number",  getNumber());
    return query.getResultList();
}
As you see this is a conventional getter method. Obviously it must return a java.util.Collection whose elements are of type Carrier.
The references in collections are bidirectional, this means that if in a Seller you have a customers collection, then in Customer you must have a reference to Seller. But it's possible that in Customer you have more than one reference to Seller (for example, seller and alternateSeller) JPA does not know which to choose, becase of this you have the attribute mappedBy of @OneToMany. You can use it in this way:
@OneToMany(mappedBy="seller")
private Collection customers;
To indicate that the reference seller and not alternateSeller will be used in this collection.
The @ManyToMany (JPA) anotation allows to define a collection with many-to-many multiplicity. As following:
@Entity
public class Customer {
    ...
    @ManyToMany
    private Collection<State> states;
    ...
}
 
In this case a customer have a collection of states, but a state can be present in several customers.

Methods

Methods are defined in an OpenXava entity (really a JPA entity) as in a regular Java class. For example:
public void increasePrice() {
    setUnitPrice(getUnitPrice().multiply(new BigDecimal("1.02")).setScale(2));
}
Methods are the sauce of the objects, without them the object only would be a silly wrapper of data. When possible it is better to put the business logic in methods (model layer) instead of in actions (controller layer).

Finders

A finder is a special static method that allows you to find an object or a collection of objects that follow some criteria.
Some examples:
public static Customer findByNumber(int number) throws NoResultException {
    Query query = XPersistence.getManager().createQuery(
    "from Customer as o where o.number = :number");
    query.setParameter("number", number);
    return (Customer) query.getSingleResult();
}
 
public static Collection findAll()  {
    Query query = XPersistence.getManager().createQuery("from Customer as o");
    return query.getResultList();
}
 
public static Collection findByNameLike(String name)  {
    Query query = XPersistence.getManager().createQuery(
    "from Customer as o where o.name like :name order by o.name desc");
    query.setParameter("name", name);
    return query.getResultList();
}
This methods can be used this way:
Customer customer = Customer.findByNumber(8);
Collection javieres = Customer.findByNameLike(%JAVI%);
As you see, using finder methods creates a more readable code than using the verbose query API of JPA. But this is only a style recomendation, you can choose do not write finder methods and to use directly JPA queries.

Entity validator

An @EntityValidator allows to define a validation at model level. When you need to make a validation on several properties at a time, and that validation does not correspond logically with any of them, then you can use this type of validation.
Its syntax is:
@EntityValidator(
    value=class,                        // 1
    onlyOnCreate=(true|false),          // 2
    properties={ @PropertyValue ... }   // 3
)
 
  1. value (required): Class that implements the validation logic. It has to be of type IValidator.
  2. onlyOnCreate (optional): If true the validator is executed only when creating a new object, not when an existing object is modified. The default value is false.
  3. properties (several @PropertyValue, optional): To set a value of the validator properties before executing it.
An example:
@EntityValidator(value=org.openxava.test.validators.CheapProductValidator.class, properties= {
    @PropertyValue(name="limit", value="100"),
    @PropertyValue(name="description"),
    @PropertyValue(name="unitPrice")
})
public class Product {
And the validator code:
package org.openxava.test.validators;
 
import java.math.*;
 
/**
 * @author Javier Paniza
 */
 
public class CheapProductValidator implements IValidator {       // 1
 
    private int limit;
    private BigDecimal unitPrice;
    private String description;
 
    public void validate(Messages errors) {                      // 2
        if (getDescription().indexOf("CHEAP") >= 0 ||
            getDescription().indexOf("BARATO") >= 0 ||
            getDescription().indexOf("BARATA") >= 0) {
            if (getLimiteBd().compareTo(getUnitPrice()) < 0) {
                errors.add("cheap_product", getLimitBd());       // 3
            }
        }
    }
 
    public BigDecimal getUnitPrice() {
        return unitPrice;
    }
 
    public void setUnitPrice(BigDecimal decimal) {
        unitPrice = decimal;
    }
 
    public String getDescription() {
        return description==null?"":description;
    }
 
    public void setDescription(String string) {
        description = string;
    }
 
    public int getLimit() {
        return limit;
    }
 
    public void setLimit(int i) {
        limit = i;
    }
 
    private BigDecimal getLimitBd() {
        return new BigDecimal(Integer.toString(limit));
    }
 
}
 
This validator must implement IValidator (1), this forces you to write a validate(Messages messages) (2). In this method you add the error message ids (3) (whose texts are in the i18n files). And if the validation process (that is the execution of all validators) produces some error, then OpenXava does not save the object and displays the errors to the user.
In this case you see how description and unitPrice properties are used to validate, for that reason the validation is at model level and not at individual property level, because the scope of validation is more than one property.
You can define more than one validator for entity using @EntityValidators, as following:
@EntityValidators({
    @EntityValidator(value=org.openxava.test.validators.CheapProductValidator.class, properties= {
        @PropertyValue(name="limit", value="100"),
        @PropertyValue(name="description"),
        @PropertyValue(name="unitPrice")
    }),
    @EntityValidator(value=org.openxava.test.validators.ExpensiveProductValidator.class, properties= {
        @PropertyValue(name="limit", value="1000"),
        @PropertyValue(name="description"),
        @PropertyValue(name="unitPrice")
    }),
    @EntityValidator(value=org.openxava.test.validators.ForbiddenPriceValidator.class,
        properties= {
            @PropertyValue(name="forbiddenPrice", value="555"),
            @PropertyValue(name="unitPrice")
        },
        onlyOnCreate=true
    )
})
public class Product {
 
@EntityValidator is defined as a Hibernate Validator contraint (new in v3.0.1).

Remove validator

The @RemoveValidator is a level model validator too, but in this case it is executed just before removing an object, and it has the possibility to deny the deletion.
Its syntax is:
@RemoveValidator(
    value=class,                        // 1
    properties={ @PropertyValue ... }   // 2
)
 
  1. class (required): Class that implements the validation logic. Must implement IRemoveValidator.
  2. properties (several @PropertyValue, optional): To set the value of the validator properties before executing it.
An example can be:
@RemoveValidator(value=DeliveryTypeRemoveValidator.class,
    properties=@PropertyValue(name="number")
)
public class DeliveryType {
 
And the validator:
package org.openxava.test.validators;
 
import org.openxava.test.model.*;
import org.openxava.util.*;
import org.openxava.validators.*;
 
/**
 * @author Javier Paniza
 */
public class DeliveryTypeRemoveValidator implements IRemoveValidator {                           // 1
 
    private DeliveryType deliveryType;
    private int number; // We use this (instaed of obtaining it from deliveryType)
                // for testing @PropertyValue for simple properties
 
    public void setEntity(Object entity) throws Exception {                                      // 2
        this.deliveryType = (DeliveryType) entity;
    }
 
    public void validate(Messages errors) throws Exception {
        if (!deliveryType.getDeliveries().isEmpty()) {
            errors.add("not_remove_delivery_type_if_in_deliveries", new Integer(getNumber()));  // 3
        }
    }
 
    public int getNumber() {
        return number;
    }
 
    public void setNumber(int number) {
        this.number = number;
    }
 
}
 
As you see this validator must implement IRemoveValidator (1) this forces you to write a setEntity() (2) method that receives the object to remove. If validation error is added to the Messages object sent to validate() (3) the validation fails. If after executing all validations there are validation errors, then OpenXava does not remove the object and displays a list of validation messages to the user.
In this case it verifies if there are deliveries that use this delivery type before deleting it.
As in the case of @EntityValidator you can use several @RemoveValidator for entity using @RemoveValidators annotation.
@RemoveValidator is executed when you remove entities from OpenXava (using MapFacade or standard OX actions), but not when you use directly JPA. If you want to create a restriction on remove which is recognized by JPA, just use @PreRemove JPA call method.

JPA callback methods

With @PrePersist you can plug in your own logic to execute just before creating the object as persistent object.
As following:
@PrePersist
private void prePersist() {
    setDescription(getDescription() + " CREATED");
}
In this case each time that a DeliveryType is created a suffix to description is added.
As you see, this is exactly the same as in other methods but is automatically executed just before creation.
With @PreUpdate you can plug in some logic to execute after the state of the object is changed and just before it is stored in the database, that is, just before executing UPDATE against database.
As following:
@PreUpdate
private void preUpdate() {
    setDescription(getDescription() + " MODIFIED");
}
In this case whenever that a DeliveryType is modified a suffix is added to its description.
As you see, this is exactly the same as in other methods, but it is executed just before modifying.
You can use all the JPA callback annotations: @PrePersist, @PostPersist, @PreRemove, @PostRemove, @PreUpdate, @PostUpdate and @PostLoad.

Embeddable classes

As stated in JPA specification:
"An entity may use other fine-grained classes to represent entity state. Instances of these classes, unlike
entity instances themselves, do not have persistent identity. Instead, they exist only as embedded objects
of the entity to which they belong. Such embedded objects belong strictly to their owning entity, and are
not sharable across persistent entities."
The embeddable class syntax is:
@Embeddable                    // 1
public class EmbeddableName {  // 2
  // Properties                // 3
  // References                // 4
  // Methods                   // 5
}
 
  1. @Embeddable (JPA, one, required): Indicates that this class is a embeddable class of JPA, in other words, its instances will be part of persistent objects.
  2. Class declaration: As a regular Java class. You can use extends and implements.
  3. Properties: Regular Java properties.
  4. References: References to entities. This is not supported by JPA 1.0 (EJB 3.0), but the Hibernate implementation support it.
  5. Methods: Java methods with the business logic.

Embedded reference

This example is an embedded (annotated with @Embedded) Address that is referenced from the main entity.
In the main entity you can write:
@Embedded
private Address address;
 
And you have to define the Address class as embeddable:
package org.openxava.test.model;
 
import javax.persistence.*;
import org.openxava.annotations.*;
 
/**
 *
 * @author Javier Paniza
 */
 
@Embeddable
public class Address implements IWithCity {  // 1
 
    @Required @Column(length=30)
    private String street;
 
    @Required @Column(length=5)
    private int zipCode;
 
    @Required @Column(length=20)
    private String city;
 
    // ManyToOne inside an Embeddable is not supported by JPA 1.0 (see at 9.1.34),
    // but Hibernate implementation supports it.
    @ManyToOne(fetch=FetchType.LAZY, optional=false) @JoinColumn(name="STATE")
    private State state;                    // 2
 
    public String getCity() {
        return city;
    }
 
    public void setCity(String city) {
        this.city = city;
    }
 
    public String getStreet() {
        return street;
    }
 
    public void setStreet(String street) {
        this.street = street;
    }
 
    public int getZipCode() {
        return zipCode;
    }
 
    public void setZipCode(int zipCode) {
        this.zipCode = zipCode;
    }
 
    public State getState() {
        return state;
    }
 
    public void setState(State state) {
        this.state = state;
    }
 
}
As you see an embeddable class can implement an interface (1) and contain references (2), among other things, but it cannot has persistent collections nor to use JPA callbacks methods.
This code can be used this way, for reading:
Customer customer = ...
Address address = customer.getAddress();
address.getStreet(); // to obtain the value
Or in this other way to set a new address:
// to set a new address
Address address = new Address();
address.setStreet(“My street”);
address.setZipCode(46001);
address.setCity(“Valencia”);
address.setState(state);
customer.setAddress(address);
In this case you have a simple reference (not collection), and the generated code is a simple JavaBean, whose life cycle is associated to its container object, that is, the Address is removed and created through the Customer. An Address never will have its own life and cannot be shared by other Customer.

Embedded collections

Embedded collections are not supported by JPA 1.0. But you can simulate them using collections to entities with cascade type REMOVE or ALL. OpenXava manages these collections in a special way, as they were embedded collections.
Now an example of an embedded collection. In the main entity (for example Invoice) you can write:
@OneToMany (mappedBy="invoice", cascade=CascadeType.REMOVE)
private Collection details;
 
Note that you use CascadeType.REMOVE, and InvoiceDetail is an entity, not an embeddable class:
package org.openxava.test.model;
 
import java.math.*;
 
import javax.persistence.*;
 
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.GenericGenerator;
import org.openxava.annotations.*;
import org.openxava.calculators.*;
import org.openxava.test.validators.*;
 
/**
 *
 * @author Javier Paniza
 */
 
@Entity
@EntityValidator(value=InvoiceDetailValidator.class,
    properties= {
        @PropertyValue(name="invoice"),
        @PropertyValue(name="oid"),
        @PropertyValue(name="product"),
        @PropertyValue(name="unitPrice")
    }
)
public class InvoiceDetail {
 
    @ManyToOne // Lazy fetching produces a fails on removing a detail from invoice
    private Invoice invoice;
 
    @Id @GeneratedValue(generator="system-uuid") @Hidden
    @GenericGenerator(name="system-uuid", strategy = "uuid")
    private String oid;
 
    private ServiceType serviceType;
    public enum ServiceType { SPECIAL, URGENT }
 
    @Column(length=4) @Required
    private int quantity;
 
    @Stereotype("MONEY") @Required
    private BigDecimal unitPrice;
 
    @ManyToOne(fetch=FetchType.LAZY, optional=false)
    private Product product;
 
    @DefaultValueCalculator(CurrentDateCalculator.class)
    private java.util.Date deliveryDate;
 
    @ManyToOne(fetch=FetchType.LAZY)
    private Seller soldBy;
 
    @Stereotype("MEMO")
    private String remarks;
 
    @Stereotype("MONEY") @Depends("unitPrice, quantity")
    public BigDecimal getAmount() {
        return getUnitPrice().multiply(new BigDecimal(getQuantity()));
    }
 
    public boolean isFree() {
        return getAmount().compareTo(new BigDecimal("0")) <= 0;
    }
 
     @PostRemove
    private void postRemove() {
        invoice.setComment(invoice.getComment() + "DETAIL DELETED");
    }
 
    public String getOid() {
        return oid;
    }
    public void setOid(String oid) {
        this.oid = oid;
    }
    public ServiceType getServiceType() {
        return serviceType;
    }
    public void setServiceType(ServiceType serviceType) {
        this.serviceType = serviceType;
    }
    public int getQuantity() {
        return quantity;
    }
    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
    public BigDecimal getUnitPrice() {
        return unitPrice==null?BigDecimal.ZERO:unitPrice;
    }
    public void setUnitPrice(BigDecimal unitPrice) {
        this.unitPrice = unitPrice;
    }
 
    public Product getProduct() {
        return product;
    }
 
    public void setProduct(Product product) {
        this.product = product;
    }
 
    public java.util.Date getDeliveryDate() {
        return deliveryDate;
    }
 
    public void setDeliveryDate(java.util.Date deliveryDate) {
        this.deliveryDate = deliveryDate;
    }
 
    public Seller getSoldBy() {
        return soldBy;
    }
 
    public void setSoldBy(Seller soldBy) {
        this.soldBy = soldBy;
    }
 
    public String getRemarks() {
        return remarks;
    }
 
    public void setRemarks(String remarks) {
        this.remarks = remarks;
    }
 
    public Invoice getInvoice() {
        return invoice;
    }
 
    public void setInvoice(Invoice invoice) {
        this.invoice = invoice;
    }
 
}
As you see this is a complex entity, with calculators, validators, references and so on. Also you have to define a reference to the container class (invoice). In this case when an Invoice is removed all its details are removed too. Moreover there are differences at user interface level (you can learn more on view chapter).

继承

OpenXava 支持Java 和 JPA继承 .
你可以按照例子定义一个@MappedSuperclass :
package org.openxava.test.model;
 
import javax.persistence.*;
 
import org.hibernate.annotations.*;
import org.openxava.annotations.*;
 
/**
 * Base class for defining entities with a UUID oid. <p>
 *
 * @author Javier Paniza
 */
 
@MappedSuperclass
public class Identifiable {
 
    @Id @GeneratedValue(generator="system-uuid") @Hidden
    @GenericGenerator(name="system-uuid", strategy = "uuid")
    private String oid;
 
    public String getOid() {
        return oid;
    }
 
    public void setOid(String oid) {
        this.oid = oid;
    }
 
}
 
你还可以定义另一个@MappedSuperclass 继承了前一个类,例如:
package org.openxava.test.model;
 
import javax.persistence.*;
 
import org.openxava.annotations.*;
 
/**
 * Base class for entities with a 'name' property. <p>
 *
 * @author Javier Paniza
 */
@MappedSuperclass
public class Nameable extends Identifiable {
 
    @Column(length=50) @Required
    private String name;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
}
 
现在你可以在你的实体类定义中使用Identifiable 或者 Nameable,如下所示:
package org.openxava.test.model;
 
import javax.persistence.*;
 
/**
 *
 * @author Javier Paniza
 */
 
@Entity
@DiscriminatorColumn(name="TYPE")
@DiscriminatorValue("HUM")
@Table(name="PERSON")
@AttributeOverrides(
    @AttributeOverride(name="name", column=@Column(name="PNAME"))
)
public class Human extends Nameable {
 
    @Enumerated(EnumType.STRING)
    private Sex sex;
    public enum Sex { MALE, FEMALE };
 
    public Sex getSex() {
        return sex;
    }
    public void setSex(Sex sex) {
        this.sex = sex;
    }
 
}
 
现在,就是实体继承,一个实体继承了另一个实体:
package org.openxava.test.model;
 
import javax.persistence.*;
 
/**
 *
 * @author Javier Paniza
 */
 
@Entity
@DiscriminatorValue("PRO")
public class Programmer extends Human {
 
    @Column(length=20)
    private String mainLanguage;
 
    public String getMainLanguage() {
        return mainLanguage;
    }
 
    public void setMainLanguage(String mainLanguage) {
        this.mainLanguage = mainLanguage;
    }
 
}
 
You can create an OpenXava module for Human and Programmer (not for Identifiable or Nameble directly). In the Programmer module the user can only access to programmers, in the other hand using Human module the user can access to Human and Programmer objects. Moreover when the user tries to view the detail of a Programmer from the Human module the Programmer view will be show. True polymorphism.
About mapping, @AttributeOverrides is supported, but, at the moment, only a single table per class hierarchy mapping strategy works.

复合键

The preferred way for defining the key of an entity is a single autogenerated key (annotated with @Id and @GeneratedValue), but sometimes, for example when you go against legate database, you need to have an entity mapped to a table that uses several column as key. This case can be solved with JPA (therefore with OpenXava) in two ways, using @IdClass or using @EmbeddedId

Id class

In this case you use @IdClass in your entity to indicate a key class, and you mark the key properties as @Id in your entity:
package org.openxava.test.model;
 
import javax.persistence.*;
 
import org.openxava.annotations.*;
import org.openxava.jpa.*;
 
/**
 *
 * @author Javier Paniza
 */
 
@Entity
@IdClass(WarehouseKey.class)
public class Warehouse {
 
    @Id
    // Column is also specified in WarehouseKey because a bug in Hibernate, see
    // http://opensource.atlassian.com/projects/hibernate/browse/ANN-361
    @Column(length=3, name="ZONE")
    private int zoneNumber;
 
    @Id @Column(length=3)
    private int number;
 
    @Column(length=40) @Required
    private String name;
 
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getNumber() {
        return number;
    }
 
    public void setNumber(int number) {
        this.number = number;
    }
 
    public int getZoneNumber() {
        return zoneNumber;
    }
 
    public void setZoneNumber(int zoneNumber) {
        this.zoneNumber = zoneNumber;
    }
 
}
 
 
You also need to declare your id class, a serializable regular class with all key properties from the entity:
package org.openxava.test.model;
 
import java.io.*;
 
import javax.persistence.*;
 
 
/**
 *
 * @author Javier Paniza
 */
 
public class WarehouseKey implements Serializable {
 
    @Column(name="ZONE")
    private int zoneNumber;
    private int number;
 
    @Override
    public boolean equals(Object obj) {
        if (obj == null) return false;
        return obj.toString().equals(this.toString());
    }
 
    @Override
    public int hashCode() {
        return toString().hashCode();
    }
 
    @Override
    public String toString() {
        return "WarehouseKey::" + zoneNumber+ ":" + number;
    }
 
    public int getNumber() {
        return number;
    }
 
    public void setNumber(int number) {
        this.number = number;
    }
 
    public int getZoneNumber() {
        return zoneNumber;
    }
 
    public void setZoneNumber(int zoneNumber) {
        this.zoneNumber = zoneNumber;
    }
 
}

Embedded id

In this case you have a reference to a @Embeddable object marked as @EmbeddedId:
package org.openxava.test.model;
 
import javax.persistence.*;
 
import org.openxava.annotations.*;
 
/**
 *
 * @author Javier Paniza
 */
 
@Entity
public class Warehouse {
 
    @EmbeddedId
    private WarehouseKey key;
 
    @Column(length=40) @Required
    private String name;
 
    public WarehouseKey getKey() {
        return key;
    }
 
    public void setKey(WarehouseKey key) {
        this.key = key;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
}
 
And you key is an embeddable class that holds the key properties:
package org.openxava.test.model;
 
import javax.persistence.*;
 
/**
 *
 * @author Javier Paniza
 */
 
@Embeddable
public class WarehouseKey implements java.io.Serializable {
 
 
    @Column(length=3, name="ZONE")
    private int zoneNumber;
 
    @Column(length=3)
    private int number;
 
    public int getNumber() {
        return number;
    }
 
    public void setNumber(int number) {
        this.number = number;
    }
 
    public int getZoneNumber() {
        return zoneNumber;
    }
 
    public void setZoneNumber(int zoneNumber) {
        this.zoneNumber = zoneNumber;
    }
 
}

Hibernate Validator (new in v3.0.1)

OpenXava has full support for Hibernate Validator. You can define your own constraints for your entities as explained in Hibernate Validator documention, and OpenXava will recognize them, showing the corresponding validation messages to the user.
Moreover, the OpenXava annotations @Required, @PropertyValidator and @EntityValidator are defined as Hibernate Validator constraints, that means that when you save an entity using directly JPA these validations will apply.
In the other hand, @RemoveValidator, @PropertyValidator(onlyOnCreate=true), EntityValidator(onlyOnCreate=true) and the default validator feature of OpenXava are not recognized by Hibernate Validator or JPA, but only by OpenXava.